//---------------------------------------------------------------------
//  This file is part of the Background Motion solution.
// 
//  Copyright (C) Mindscape (TM).  All rights reserved.
//  http://www.mindscape.co.nz
// 
//  THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//---------------------------------------------------------------------

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Web;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.WebControls;

using Mindscape.Web.UI.WebControls.Exceptions;
using Mindscape.Web.UI.WebControls.Interfaces;

namespace Mindscape.Web.UI.WebControls.Binding
{
  [NonVisualControl]
  [Designer(typeof(WebDataBinderDesigner))]
  [ProvideProperty("BindingItem", typeof(Control))]
  [ParseChildren(true, "BindingItems")]
  [PersistChildren(false)]
  [DefaultProperty("BindingItems")]
  [DefaultEvent("ValidateControl")]
  public class WebDataBinder : Control, IExtenderProvider
  {
    private WebDataBindingItemCollection _bindingItems = null;
    private bool _autoLoadDataBoundControls = false;
    private bool _autoLoadedDataBoundControls = false;

    public WebDataBinder()
    {
      _bindingItems = new WebDataBindingItemCollection(this);
    }

    public new bool DesignMode = (HttpContext.Current == null);

    /// <summary>
    /// Gets the binding items.
    /// </summary>
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public WebDataBindingItemCollection BindingItems
    {
      get { return _bindingItems; }
    }

    /// <summary>
    /// Automatically imports all controls on the form that implement the IWebDataBinder interface and adds them to the DataBinder
    /// </summary>
    /// <value>
    /// 	<c>true</c> if [auto load data bound controls]; otherwise, <c>false</c>.
    /// </value>
    [Description(
      "Automatically imports all controls on the form that implement the IWebDataBinder interface and adds them to the DataBinder"
      )]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool AutoLoadDataBoundControls
    {
      get { return _autoLoadDataBoundControls; }
      set { _autoLoadDataBoundControls = value; }
    }

    /// <summary>
    /// Event that can be hooked to validate each control after it's been unbound. 
    /// Allows for doing application level validation of the data once it's been 
    /// returned.
    /// 
    /// This method receives a WebDataBindingItem parameter which includes a 
    /// reference to both the control and the DataSource object where you can check
    ///  values. Return false from the event method to indicate that validation 
    /// failed which causes a new BindingError to be created and added to the 
    /// BindingErrors collection.
    /// </summary>        
    [Description(
      "Fired after a control has been unbound. Gets passed an instance of the DataBinding item. Event must check for the control to check."
      )]
    public event ItemResultNotification ValidateControl;

    /// <summary>
    /// Fired just before the control is bound. You can return false from the 
    /// handler to cause the control to not be bound
    /// </summary>
    [Description("Fires immediately before a control is bound. Fires for all controls and is passed a DataBindingItem.")
    ]
    public event ItemResultNotification BeforeBindControl;

    /// <summary>
    /// Fires immediately after the control has been bound. You can check for
    /// errors or perform additional validation.
    /// </summary>
    [Description(
      "Fires immediately after a control has been bound. Gets passed a DataBinding Item instance. Fires for all bound controls."
      )]
    public event ItemNotification AfterBindControl;

    /// <summary>
    /// Fires immediately before unbinding of a control takes place.
    /// You can return false to abort DataUnbinding.wl
    /// </summary
    [Description(
      "Fires immediately before a control is unbound. Gets passed a DataBinding Item instance. Fires for all bound controls."
      )]
    public event ItemResultNotification BeforeUnbindControl;

    /// <summary>
    /// Fires immediately after binding is complete. You can check for errors 
    /// and take additional action. 
    /// </summary>
    [Description(
      "Fires immediately after a control has been unbound. Gets passed a DataBinding Item instance. Fires for all bound controls."
      )]
    public event ItemNotification AfterUnbindControl;

    /// <summary>
    /// Binds the controls that are attached to this DataBinder.
    /// </summary>
    /// <returns>true if there no errors. False otherwise.</returns>
    public new bool DataBind()
    {
      return DataBind(Page);
    }

    /// <summary>
    /// Binds data of the specified controls into the specified BindingSource
    /// </summary>
    /// <param name="Container">The top level container that is bound</param>
    public bool DataBind(Control Container)
    {
      if (AutoLoadDataBoundControls)
      {
        LoadFromControls(Container);
      }

      bool result = true;

      foreach (WebDataBindingItem Item in BindingItems)
      {
        try
        {
          if (BeforeBindControl != null)
          {
            if (!BeforeBindControl(Item))
            {
              continue;
            }
          }

          Item.DataBind(Container);

          if (AfterBindControl != null)
          {
            AfterBindControl(Item);
          }
        }
        catch (Exception ex)
        {
          HandleBindingError(Item, ex);
          result = false;
        }
      }

      return result;
    }

    /// <summary>
    /// Unbind the controls back into their underlying binding sources. Returns true on success
    /// false on failure. On failure the BindingErrors collection will be set
    /// </summary>
    /// <returns>True if there are no errors. False if unbinding failed. Check the BindingErrors for errors.</returns>
    public bool Unbind()
    {
      return Unbind(Page);
    }

    /// <summary>
    /// Unbind the controls back into their binding source. Returns true on success
    /// false on failure. On failure the BindingErrors collection will be set
    /// </summary>
    /// <param name="Container">The top level container Control that is bound.</param>
    /// <returns>True if there are no errors. False if unbinding failed. Check the BindingErrors for errors.</returns>
    public bool Unbind(Control Container)
    {
      if (AutoLoadDataBoundControls)
      {
        LoadFromControls(Container);
      }

      bool result = true;

      foreach (WebDataBindingItem Item in BindingItems)
      {
        try
        {
          if (BeforeUnbindControl != null)
          {
            if (!BeforeUnbindControl(Item))
            {
              continue;
            }
          }

          Item.Unbind(Container);

          // Validate control
          if (!OnValidateControl(Item))
          {
            HandleUnbindingError(Item, new WebValidationException("Error with unbinding - validation failed"));
          }

          if (!Item.OnValidate())
          {
            HandleUnbindingError(Item, new WebValidationException("Error with unbinding - validation failed"));
          }

          if (AfterUnbindControl != null)
          {
            AfterUnbindControl(Item);
          }
        }
        catch (Exception ex)
        {
          HandleUnbindingError(Item, ex);
          result = false;
        }
      }

      foreach (IValidator Validator in Page.Validators)
      {
        if (Validator.IsValid)
        {
          continue;
        }

        string ClientId = null;

        BaseValidator BValidator = Validator as BaseValidator;
        if (BValidator != null)
        {
          Control Ctl = WebDataBindingHelpers.FindControlRecursive(Page, BValidator.ControlToValidate);
          if (Ctl != null)
          {
            ClientId = Ctl.ClientID;
          }
        }
      }

      return result;
    }

    private void HandleBindingError(WebDataBindingItem Item, Exception ex)
    {
      // TODO: Do we need to handle anything?
    }

    private void HandleUnbindingError(WebDataBindingItem Item, Exception ex)
    {
      // TODO: Do we need to handle anything?
    }

    /// <summary>
    /// Adds a binding to the control. This method is a simple
    /// way to establish a binding.
    /// 
    /// Returns the Item so you can customize properties further
    /// </summary>
    /// <param name="ControlToBind"></param>
    /// <param name="ControlPropertyToBind"></param>
    /// <param name="SourceObjectToBindTo"></param>
    /// <param name="SourceMemberToBindTo"></param>
    public WebDataBindingItem AddBinding(Control ControlToBind, string ControlPropertyToBind,
      object SourceObjectToBindTo, string SourceMemberToBindTo)
    {
      WebDataBindingItem Item = new WebDataBindingItem(this);

      Item.TargetControlInstance = ControlToBind;
      Item.TargetControl = ControlToBind.ID;
      Item.BindingSourceObject = SourceObjectToBindTo;
      Item.BindingSourceMember = SourceMemberToBindTo;

      BindingItems.Add(Item);

      return Item;
    }

    /// <summary>
    /// Adds a binding to the control. This method is a simple
    /// way to establish a binding.
    /// 
    /// Returns the Item so you can customize properties further
    /// </summary>
    /// <param name="ControlToBind"></param>
    /// <param name="ControlPropertyToBind"></param>
    /// <param name="SourceObjectNameToBindTo"></param>
    /// <param name="SourceMemberToBindTo"></param>
    /// <returns></returns>
    public WebDataBindingItem AddBinding(Control ControlToBind, string ControlPropertyToBind,
      string SourceObjectNameToBindTo, string SourceMemberToBindTo)
    {
      WebDataBindingItem Item = new WebDataBindingItem(this);

      Item.TargetControlInstance = ControlToBind;
      Item.TargetControl = ControlToBind.ID;
      Item.Page = Page;
      Item.BindingSource = SourceObjectNameToBindTo;
      Item.BindingSourceMember = SourceMemberToBindTo;

      BindingItems.Add(Item);

      return Item;
    }

    /// <summary>
    /// This method only adds a data binding item, but doesn't bind it
    /// to anything. This can be useful for only displaying errors
    /// </summary>
    /// <param name="ControlToBind"></param>
    /// <returns></returns>
    public WebDataBindingItem AddBinding(Control ControlToBind)
    {
      WebDataBindingItem Item = new WebDataBindingItem(this);

      Item.TargetControlInstance = ControlToBind;
      Item.TargetControl = ControlToBind.ID;
      Item.Page = Page;

      BindingItems.Add(Item);

      return Item;
    }

    /// <summary>
    /// Picks up all controls on the form that implement the IWebDataBinder interface
    /// and adds them to the DataBindingItems Collection
    /// </summary>
    /// <param name="Container"></param>
    /// <returns></returns>
    public void LoadFromControls(Control Container)
    {
      if (_autoLoadedDataBoundControls)
      {
        return;
      }
      _autoLoadedDataBoundControls = true;

      LoadDataBoundControls(Container);
    }

    /// <summary>
    /// Loop through all of the contained controls of the form and
    /// check for all that implement IWebDataBinder. If found
    /// add the BindingItem to this Databinder
    /// </summary>
    /// <param name="Container"></param>
    private void LoadDataBoundControls(Control Container)
    {
      foreach (Control control in Container.Controls)
      {
        if (control.Controls.Count > 0)
        {
          LoadDataBoundControls(control);
        }

        if (control is IWebDataBinder)
        {
          BindingItems.Add(((IWebDataBinder)control).BindingItem);
        }
      }
    }

    /// <summary>
    /// Fires the ValidateControlEvent
    /// </summary>
    /// <param name="Item"></param>
    /// <returns>false - Validation for control failed and a BindingError is added, true - Validation succeeded</returns>
    public bool OnValidateControl(WebDataBindingItem Item)
    {
      if (ValidateControl != null && !ValidateControl(Item))
      {
        return false;
      }

      return true;
    }

    #region IExtenderProvider Members

    public bool CanExtend(object extendee)
    {
      if (extendee is WebDataBinder)
      {
        return false;
      }
      if (extendee is Control)
      {
        return true;
      }

      return false;
    }

    /// <summary>
    /// Returns a specific DataBinding Item for a given control.
    /// Always returns an item even if the Control is not found.
    /// If you need to check whether this is a valid item check
    /// the BindingSource property for being blank.
    /// 
    /// Extender Property Get method
    /// </summary>
    /// <param name="control"></param>
    /// <returns></returns>
    public WebDataBindingItem GetDataBindingItem(Control control)
    {
      foreach (WebDataBindingItem Item in BindingItems)
      {
        if (Item.TargetControl == control.ID)
        {
          // *** Ensure the binder is set on the item
          Item.Binder = this;
          return Item;
        }
      }

      WebDataBindingItem NewItem = new WebDataBindingItem(this);
      NewItem.TargetControl = control.ID;
      NewItem.TargetControlInstance = control;

      BindingItems.Add(NewItem);

      return NewItem;
    }

    /// <summary>
    /// Return a specific databinding item for a give control id.
    /// Note unlike the ControlInstance version return null if the
    /// ControlId isn't found. 
    /// </summary>
    /// <param name="ControlId"></param>
    /// <returns></returns>
    public WebDataBindingItem GetDataBindingItem(string ControlId)
    {
      for (int i = 0; i < BindingItems.Count; i++)
      {
        if (BindingItems[i].TargetControl == ControlId)
        {
          return BindingItems[i];
        }
      }

      return null;
    }

    /// <summary>
    /// this method is used to ensure that designer is notified
    /// every time there is a change in the sub-ordinate validators
    /// </summary>
    internal void NotifyDesigner()
    {
      if (DesignMode)
      {
        IDesignerHost Host = Site.Container as IDesignerHost;
        ControlDesigner Designer = Host.GetDesigner(this) as ControlDesigner;
        PropertyDescriptor Descriptor = null;
        try
        {
          Descriptor = TypeDescriptor.GetProperties(this)["DataBindingItems"];
        }
        catch
        {
          return;
        }

        ComponentChangedEventArgs ccea = new ComponentChangedEventArgs(
          this,
          Descriptor,
          null,
          BindingItems);
        Designer.OnComponentChanged(this, ccea);
      }
    }

    #endregion
  }

  public delegate bool ItemResultNotification(WebDataBindingItem Item);

  public delegate void ItemNotification(WebDataBindingItem Item);

  public delegate void DataBindingItemValidate(object sender, DataBindingValidationEventArgs e);

  /// <summary>
  /// Control designer used so we get a grey button display instead of the 
  /// default label display for the control.
  /// </summary>
  internal class WebDataBinderDesigner : ControlDesigner
  {
    public override string GetDesignTimeHtml()
    {
      return base.CreatePlaceHolderDesignTimeHtml("Control Extender");
    }
  }
}